Files
aliasvault/Dockerfile.single
2025-08-11 13:18:45 +02:00

347 lines
16 KiB
Docker

# Multi-stage build for AliasVault single container deployment
# This creates a single container with all services for NAS/home server deployments
# ============================================
# Stage 1: Build .NET applications
# ============================================
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS dotnet-builder
# Install Python (required for WASM compilation)
RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*
WORKDIR /src
# Copy all source files
COPY apps/server/ ./apps/server/
COPY shared/ ./shared/
# Install required .NET workloads
RUN dotnet workload install wasm-tools
# Build all .NET applications
WORKDIR /src/apps/server
# Build API
RUN dotnet publish AliasVault.Api/AliasVault.Api.csproj -c Release -o /app/api
# Build Client (requires wasm-tools workload)
RUN dotnet publish AliasVault.Client/AliasVault.Client.csproj -c Release -o /app/client
# Build Admin
RUN dotnet publish AliasVault.Admin/AliasVault.Admin.csproj -c Release -o /app/admin
# Build SMTP Service
RUN dotnet publish Services/AliasVault.SmtpService/AliasVault.SmtpService.csproj -c Release -o /app/smtp
# Build Task Runner
RUN dotnet publish Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj -c Release -o /app/taskrunner
# ============================================
# Stage 2: Final runtime image with s6-overlay
# ============================================
FROM mcr.microsoft.com/dotnet/aspnet:9.0
# Install required packages
RUN apt-get update && apt-get install -y \
nginx \
postgresql-15 \
postgresql-client-15 \
openssl \
curl \
xz-utils \
netcat-openbsd \
gettext-base \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -r -s /bin/bash -d /var/lib/postgresql postgres 2>/dev/null || true
# Install s6-overlay v3
ARG S6_OVERLAY_VERSION=3.2.0.2
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
&& tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz \
&& rm /tmp/s6-overlay-*.tar.xz
# Create necessary directories
RUN mkdir -p \
/app/api \
/app/client \
/app/admin \
/app/smtp \
/app/taskrunner \
/data/postgres \
/data/database \
/data/certificates/ssl \
/data/certificates/app \
/data/certificates/letsencrypt \
/data/logs \
/etc/nginx/ssl \
/var/run/postgresql \
/var/www/certbot
# Copy built applications from builder stage
COPY --from=dotnet-builder /app/api /app/api
COPY --from=dotnet-builder /app/client /app/client
COPY --from=dotnet-builder /app/admin /app/admin
COPY --from=dotnet-builder /app/smtp /app/smtp
COPY --from=dotnet-builder /app/taskrunner /app/taskrunner
# Copy client nginx configuration and ensure wwwroot is accessible
COPY apps/server/AliasVault.Client/nginx.conf /app/client/nginx.conf
# Copy nginx configuration for single container deployment
COPY apps/server/nginx.single.conf /etc/nginx/nginx.single.conf
# ============================================
# S6 Service Definitions
# ============================================
# Container initialization
RUN mkdir -p /etc/s6-overlay/s6-rc.d/init-container && \
echo '#!/bin/sh' > /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'echo "[init-container] Initializing AliasVault single container..." >&2' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'echo "[init-container] Creating data directories..." >&2' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'mkdir -p /data/database /data/logs /data/certificates /data/postgres' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo '' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'echo "[init-container] Creating symbolic links for persistent data..." >&2' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'ln -sf /data/database /database' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'ln -sf /data/logs /logs' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'ln -sf /data/certificates /certificates' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo '' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'echo "[init-container] Setting database permissions..." >&2' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'chown -R postgres:postgres /data/postgres 2>/dev/null || true' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'chmod 700 /data/postgres 2>/dev/null || true' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo '' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
echo 'echo "[init-container] Container initialization complete" >&2' >> /etc/s6-overlay/s6-rc.d/init-container/up && \
chmod +x /etc/s6-overlay/s6-rc.d/init-container/up && \
echo "oneshot" > /etc/s6-overlay/s6-rc.d/init-container/type
# PostgreSQL service
RUN mkdir -p /etc/s6-overlay/s6-rc.d/postgres && \
{ echo '#!/bin/sh'; \
echo ''; \
echo '# Set PostgreSQL paths'; \
echo 'export PATH="/usr/lib/postgresql/15/bin:$PATH"'; \
echo 'export PGDATA="/data/postgres"'; \
echo ''; \
echo '# Initialize PostgreSQL if needed'; \
echo 'if [ ! -d "$PGDATA/base" ]; then'; \
echo ' echo "Initializing PostgreSQL database..."'; \
echo ' mkdir -p "$PGDATA" /data/logs'; \
echo ' chown -R postgres:postgres "$PGDATA" /data/logs'; \
echo ' chmod 700 "$PGDATA"'; \
echo ' su - postgres -c "/usr/lib/postgresql/15/bin/initdb -D $PGDATA"'; \
echo ' '; \
echo ' # Configure PostgreSQL'; \
echo ' echo "host all all 127.0.0.1/32 md5" >> "$PGDATA/pg_hba.conf"'; \
echo ' echo "listen_addresses = '\''127.0.0.1'\''" >> "$PGDATA/postgresql.conf"'; \
echo ' '; \
echo ' # Start PostgreSQL temporarily to create database and user'; \
echo ' su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D $PGDATA -l /data/logs/postgres.log start"'; \
echo ' sleep 5'; \
echo ' '; \
echo ' # Create database and user'; \
echo ' su - postgres -c "/usr/lib/postgresql/15/bin/psql -c \\"CREATE USER aliasvault WITH PASSWORD '\''${POSTGRES_PASSWORD:-defaultpassword}'\''\\""'; \
echo ' su - postgres -c "/usr/lib/postgresql/15/bin/psql -c \\"CREATE DATABASE aliasvault OWNER aliasvault;\\""'; \
echo ' su - postgres -c "/usr/lib/postgresql/15/bin/psql -c \\"GRANT ALL PRIVILEGES ON DATABASE aliasvault TO aliasvault;\\""'; \
echo ' '; \
echo ' # Stop PostgreSQL'; \
echo ' su - postgres -c "/usr/lib/postgresql/15/bin/pg_ctl -D $PGDATA stop"'; \
echo ' sleep 2'; \
echo 'fi'; \
echo ''; \
echo '# Run PostgreSQL'; \
echo 'exec s6-setuidgid postgres /usr/lib/postgresql/15/bin/postgres -D "$PGDATA"'; \
} > /etc/s6-overlay/s6-rc.d/postgres/run && \
chmod +x /etc/s6-overlay/s6-rc.d/postgres/run && \
echo "longrun" > /etc/s6-overlay/s6-rc.d/postgres/type && \
mkdir -p /etc/s6-overlay/s6-rc.d/postgres/dependencies.d && \
touch /etc/s6-overlay/s6-rc.d/postgres/dependencies.d/init-container
# Wait for PostgreSQL to be ready
RUN mkdir -p /etc/s6-overlay/s6-rc.d/postgres-ready && \
{ echo '#!/bin/sh'; \
echo 'echo "Waiting for PostgreSQL to be ready..."'; \
echo 'for i in {1..30}; do'; \
echo ' if su - postgres -c "/usr/lib/postgresql/15/bin/pg_isready -h localhost" > /dev/null 2>&1; then'; \
echo ' echo "PostgreSQL is ready"'; \
echo ' exit 0'; \
echo ' fi'; \
echo ' sleep 1'; \
echo 'done'; \
echo 'echo "PostgreSQL failed to start"'; \
echo 'exit 1'; \
} > /etc/s6-overlay/s6-rc.d/postgres-ready/up && \
chmod +x /etc/s6-overlay/s6-rc.d/postgres-ready/up && \
echo "oneshot" > /etc/s6-overlay/s6-rc.d/postgres-ready/type && \
mkdir -p /etc/s6-overlay/s6-rc.d/postgres-ready/dependencies.d && \
touch /etc/s6-overlay/s6-rc.d/postgres-ready/dependencies.d/postgres
# API service
RUN mkdir -p /etc/s6-overlay/s6-rc.d/api && \
{ echo '#!/command/with-contenv bash'; \
echo 'cd /app/api'; \
echo 'export ConnectionStrings__AliasServerDbContext="Host=localhost;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD:-defaultpassword}"'; \
echo 'export ASPNETCORE_URLS="http://0.0.0.0:3001"'; \
echo 'exec dotnet AliasVault.Api.dll'; \
} > /etc/s6-overlay/s6-rc.d/api/run && \
chmod +x /etc/s6-overlay/s6-rc.d/api/run && \
echo "longrun" > /etc/s6-overlay/s6-rc.d/api/type && \
mkdir -p /etc/s6-overlay/s6-rc.d/api/dependencies.d && \
touch /etc/s6-overlay/s6-rc.d/api/dependencies.d/postgres-ready
# Client service (nginx for WASM app) - using echo approach
RUN mkdir -p /etc/s6-overlay/s6-rc.d/client && \
{ echo '#!/command/with-contenv bash'; \
echo '# Client service entrypoint'; \
echo 'DEFAULT_PRIVATE_EMAIL_DOMAINS="localmail.tld"'; \
echo 'DEFAULT_SUPPORT_EMAIL=""'; \
echo 'PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS}'; \
echo 'SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL}'; \
echo ''; \
echo 'mkdir -p /etc/nginx/ssl'; \
echo ''; \
echo 'if [ ! -f /etc/nginx/ssl/nginx.crt ] || [ ! -f /etc/nginx/ssl/nginx.key ]; then'; \
echo ' echo "Generating SSL certificate..."'; \
echo ' openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"'; \
echo ' chmod 644 /etc/nginx/ssl/nginx.crt'; \
echo ' chmod 600 /etc/nginx/ssl/nginx.key'; \
echo 'fi'; \
echo ''; \
echo '# Create simple JSON with environment variables'; \
echo 'cat > /app/client/wwwroot/appsettings.json << EOF'; \
echo '{'; \
echo ' "PrivateEmailDomains": ["$PRIVATE_EMAIL_DOMAINS"],'; \
echo ' "SupportEmail": "$SUPPORT_EMAIL",'; \
echo ' "PublicRegistrationEnabled": "$PUBLIC_REGISTRATION_ENABLED"'; \
echo '}'; \
echo 'EOF'; \
echo ''; \
echo 'sed -i "s|/usr/share/nginx/html|/app/client/wwwroot|g" /app/client/nginx.conf'; \
echo ''; \
echo 'exec nginx -c /app/client/nginx.conf -g "daemon off;"'; \
} > /etc/s6-overlay/s6-rc.d/client/run && \
chmod +x /etc/s6-overlay/s6-rc.d/client/run && \
echo "longrun" > /etc/s6-overlay/s6-rc.d/client/type
# Admin service
RUN mkdir -p /etc/s6-overlay/s6-rc.d/admin && \
{ echo '#!/command/with-contenv bash'; \
echo 'cd /app/admin'; \
echo 'export ConnectionStrings__AliasServerDbContext="Host=localhost;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD:-defaultpassword}"'; \
echo 'export ASPNETCORE_URLS="http://0.0.0.0:3002"'; \
echo 'exec dotnet AliasVault.Admin.dll'; \
} > /etc/s6-overlay/s6-rc.d/admin/run && \
chmod +x /etc/s6-overlay/s6-rc.d/admin/run && \
echo "longrun" > /etc/s6-overlay/s6-rc.d/admin/type && \
mkdir -p /etc/s6-overlay/s6-rc.d/admin/dependencies.d && \
touch /etc/s6-overlay/s6-rc.d/admin/dependencies.d/postgres-ready
# SMTP service
RUN mkdir -p /etc/s6-overlay/s6-rc.d/smtp && \
{ echo '#!/command/with-contenv bash'; \
echo 'cd /app/smtp'; \
echo 'export ConnectionStrings__AliasServerDbContext="Host=localhost;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD:-defaultpassword}"'; \
echo 'exec dotnet AliasVault.SmtpService.dll'; \
} > /etc/s6-overlay/s6-rc.d/smtp/run && \
chmod +x /etc/s6-overlay/s6-rc.d/smtp/run && \
echo "longrun" > /etc/s6-overlay/s6-rc.d/smtp/type && \
mkdir -p /etc/s6-overlay/s6-rc.d/smtp/dependencies.d && \
touch /etc/s6-overlay/s6-rc.d/smtp/dependencies.d/postgres-ready
# Task Runner service
RUN mkdir -p /etc/s6-overlay/s6-rc.d/taskrunner && \
{ echo '#!/command/with-contenv bash'; \
echo 'cd /app/taskrunner'; \
echo 'export ConnectionStrings__AliasServerDbContext="Host=localhost;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD:-defaultpassword}"'; \
echo 'exec dotnet AliasVault.TaskRunner.dll'; \
} > /etc/s6-overlay/s6-rc.d/taskrunner/run && \
chmod +x /etc/s6-overlay/s6-rc.d/taskrunner/run && \
echo "longrun" > /etc/s6-overlay/s6-rc.d/taskrunner/type && \
mkdir -p /etc/s6-overlay/s6-rc.d/taskrunner/dependencies.d && \
touch /etc/s6-overlay/s6-rc.d/taskrunner/dependencies.d/postgres-ready
# Nginx service
RUN mkdir -p /etc/s6-overlay/s6-rc.d/nginx && \
{ echo '#!/command/with-contenv bash'; \
echo '# Generate SSL certificate if not exists'; \
echo 'if [ ! -f /data/certificates/ssl/cert.pem ]; then'; \
echo ' echo "Generating self-signed SSL certificate..."'; \
echo ' mkdir -p /data/certificates/ssl'; \
echo ' openssl req -x509 -nodes -days 365 -newkey rsa:2048 \\'; \
echo ' -keyout /data/certificates/ssl/key.pem \\'; \
echo ' -out /data/certificates/ssl/cert.pem \\'; \
echo ' -subj "/C=US/ST=State/L=City/O=Organization/CN=${HOSTNAME:-localhost}"'; \
echo 'fi'; \
echo ''; \
echo '# Copy certificates to nginx directory'; \
echo 'cp /data/certificates/ssl/* /etc/nginx/ssl/ 2>/dev/null || true'; \
echo ''; \
echo '# Create SSL configuration file'; \
echo 'cat > /etc/nginx/ssl.conf << "SSLEOF"'; \
echo 'ssl_certificate /etc/nginx/ssl/cert.pem;'; \
echo 'ssl_certificate_key /etc/nginx/ssl/key.pem;'; \
echo 'ssl_session_timeout 1d;'; \
echo 'ssl_session_cache shared:MozTLS:10m;'; \
echo 'ssl_session_tickets off;'; \
echo 'ssl_protocols TLSv1.2 TLSv1.3;'; \
echo 'ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;'; \
echo 'ssl_prefer_server_ciphers off;'; \
echo 'SSLEOF'; \
echo ''; \
echo '# Use single container nginx configuration'; \
echo 'cp /etc/nginx/nginx.single.conf /etc/nginx/nginx.conf'; \
echo ''; \
echo '# Wait for all services to be ready'; \
echo 'echo "Waiting for services to be ready..."'; \
echo 'for i in {1..60}; do'; \
echo ' if nc -z localhost 3000 && nc -z localhost 3001 && nc -z localhost 3002; then'; \
echo ' echo "All services ready, starting nginx..."'; \
echo ' break'; \
echo ' fi'; \
echo ' if [ $i -eq 60 ]; then'; \
echo ' echo "Timeout waiting for services"'; \
echo ' exit 1'; \
echo ' fi'; \
echo ' sleep 1'; \
echo 'done'; \
echo ''; \
echo 'exec nginx -g "daemon off;"'; \
} > /etc/s6-overlay/s6-rc.d/nginx/run && \
chmod +x /etc/s6-overlay/s6-rc.d/nginx/run && \
echo "longrun" > /etc/s6-overlay/s6-rc.d/nginx/type && \
mkdir -p /etc/s6-overlay/s6-rc.d/nginx/dependencies.d && \
touch /etc/s6-overlay/s6-rc.d/nginx/dependencies.d/api && \
touch /etc/s6-overlay/s6-rc.d/nginx/dependencies.d/client && \
touch /etc/s6-overlay/s6-rc.d/nginx/dependencies.d/admin
# Enable all services in the default bundle
RUN mkdir -p /etc/s6-overlay/s6-rc.d/user/contents.d && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/init-container && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/postgres && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/postgres-ready && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/api && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/client && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/admin && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/smtp && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/taskrunner && \
touch /etc/s6-overlay/s6-rc.d/user/contents.d/nginx
# Set environment variables for s6-overlay
ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_VERBOSITY=1 \
S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \
S6_KILL_GRACETIME=30000
# Expose ports
EXPOSE 80 443 25 587
# Volume for persistent data
VOLUME ["/data"]
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
CMD curl -f -k https://localhost/api || exit 1
# Set s6-overlay as entrypoint
ENTRYPOINT ["/init"]