Skip to content

Docker Deployment

Deploy Deckyard using Docker and Docker Compose for easy setup and management.

  • Docker - Version 20.10 or later
  • Docker Compose - Version 2.0 or later
  • Domain name - For HTTPS (production)
  • DNS configured - Pointing to your server
Terminal window
git clone https://github.com/jaapstronks/deckyard.git
cd deckyard

Copy the example environment file:

Terminal window
cp .env.example .env

Edit .env with your configuration:

Terminal window
# Required unless you set AUTH_ENABLED=false (at least 32 characters)
AUTH_SECRET=your-random-secret-key-here
AUTH_ADMIN_EMAIL=admin@yourdomain.com
# Domain (for Caddy HTTPS)
DOMAIN=slides.yourdomain.com
LETSENCRYPT_EMAIL=admin@yourdomain.com
# Optional: Email
BREVO_API_KEY=your-brevo-api-key
BREVO_SENDER_EMAIL=slides@yourdomain.com
BREVO_SENDER_NAME=Deckyard
# Optional: AI features
OPENAI_API=sk-...
# Optional: Media storage
IMAGEKIT_PUBLIC_KEY=...
IMAGEKIT_PRIVATE_KEY=...
IMAGEKIT_URL_ENDPOINT=...

The server refuses to start without an AUTH_SECRET of at least 32 characters unless you opt out of authentication with AUTH_ENABLED=false (see Authentication). The database needs no settings: the stack brings its own.

Terminal window
docker compose up -d

This starts:

  • app - The Deckyard application on port 4177 (reachable only on the compose network)
  • postgres - PostgreSQL 16, the database; migrations run automatically when the app container starts
  • caddy - Reverse proxy with automatic HTTPS

Open https://your-domain.com in your browser.

docker-compose.yml in the repository is the production topology: app behind caddy on ports 80 and 443, with postgres next to it. The app only exposes port 4177 on the compose network, so a bare docker compose up does not publish http://localhost:4177. For a local run without a domain, add the local overlay, which publishes 4177 (and the database on 5433) and leaves Caddy out:

Terminal window
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build

Every DATABASE_* value in the compose file can be overridden from .env; Database Configuration explains the defaults and how to use a managed database instead.

VolumePurpose
pg_dataThe PostgreSQL database: presentations, users, settings and everything else except media
./server/uploadsUploaded images and files
./server/dataDeck-thumbnail cache, plus files left by an install from before PostgreSQL
caddy_dataSSL certificates and Caddy state
caddy_configCaddy configuration cache
{
email {$LETSENCRYPT_EMAIL}
}
{$DOMAIN} {
encode gzip
reverse_proxy app:4177
}

Caddy automatically:

  • Obtains SSL certificates from Let’s Encrypt
  • Renews certificates before expiry
  • Redirects HTTP to HTTPS
  • Enables gzip compression

Set these in your .env file:

Terminal window
DOMAIN=slides.yourdomain.com
LETSENCRYPT_EMAIL=admin@yourdomain.com

To serve multiple domains:

{
email {$LETSENCRYPT_EMAIL}
}
slides.example.com, presentations.example.com {
encode gzip
reverse_proxy app:4177
}

The included Dockerfile builds on node:22-alpine and adds:

  • Chromium (plus an emoji font) - for server-side PNG, PDF and PPTX exports
  • A non-root user - the app runs as the image’s node user
  • An entrypoint - applies pending database migrations, then starts the server

Add resource limits in docker-compose.yml:

services:
app:
# ... other config
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M

Add health checks for reliability:

services:
app:
# ... other config
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4177/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

Configure log rotation:

services:
app:
# ... other config
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

The stack’s own postgres service is the default. To use a managed or existing PostgreSQL instead, set its connection in .env, for example:

Terminal window
DATABASE_URL=postgres://user:pass@your-db-host:5432/deckyard

Do not set DATABASE_HOST=localhost here: inside the container that is the app itself, and the entrypoint refuses to start. See Database Configuration for SSL and pool settings.

Terminal window
git pull origin main
docker compose build
docker compose up -d

Pending migrations run when the new app container starts.

To rebuild and restart the app without touching the database and Caddy containers:

Terminal window
docker compose up -d --no-deps --build app

The app is briefly unavailable while its container is replaced.

Back up the database and the uploads together:

Terminal window
docker compose exec postgres sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backup-$(date +%Y%m%d).sql
tar -czvf uploads-$(date +%Y%m%d).tar.gz ./server/uploads

With a managed database, back that up through your provider instead. server/data holds only a cache and needs no backup once any old file-based data has been imported (Upgrading from file-based storage).

Terminal window
# All services
docker compose logs -f
# Specific service
docker compose logs -f app
docker compose logs -f caddy
Terminal window
docker compose ps
Terminal window
docker compose exec app sh

If Caddy fails to get certificates:

  1. Check domain DNS is pointing to your server
  2. Verify ports 80 and 443 are open
  3. Check Caddy logs: docker compose logs caddy

If the app crashes due to memory:

  1. Increase memory limits in docker-compose.yml
  2. Use external media storage

For a local run with Docker, use the local overlay described under Default Configuration:

Terminal window
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build

Or run without Docker for faster iteration.