Support Scaffold

Support Scaffold on GitHub Sponsors – Helping developers start projects faster.

DevOps

Deploying to production

Create a new server instance

Spin up a new server using your cloud provider (link to Digital Ocean) and connect to your server instance via its public IP address

Configure UFW (Uncomplicated Firewall) to allow ports 22, 80 & 443

# Add rules
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https

# Check what's configured
sudo ufw show added

# Enable the firewall
sudo ufw enable

Install Docker

Set up Docker's apt repository.

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

Install Docker packages

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Allow non-root user to run docker

sudo usermod -aG docker ubuntu

You will need to log out and back in again, but once you are logged in again you should be able to run docker ps successfully as the ubuntu user.

Setup your site (single project)

Project Folder

Create the folder to house your site

# folder for the projects
sudo mkdir -p /data/sites

# Allow the ubuntu user write access to the /data folder
sudo chown ubuntu:ubuntu /data

then go into this directory

cd /opt/sites/supercoolwidgets.xyz

Multi-Project Server (multi project)

# folder for the docker compose services
sudo mkdir -p /data/services

Create a Docker compose file (/data/services/compose.yaml)

services:
  traefik:
    image: traefik:v3.4.3
    container_name: traefik
    restart: unless-stopped
    networks:
      - web
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080" # optional dashboard
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik_letsencrypt_data:/letsencrypt
    command:
      # ── discovery ───────────────────────────────────────────────
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      # ── entrypoints ─────────────────────────────────────────────
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.websecure.address=:443"
      # ── ACME / Let’s Encrypt ────────────────────────────────────
      - "--certificatesresolvers.le.acme.email=[... YOUR EMAIL ADDRESS ...]"
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.le.acme.httpchallenge=true"
      - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"

networks:
  web:
    external: true

volumes:
  traefik_letsencrypt_data:

Note: The optional dashboard is secure as we don't expose port 8080 (only 80, 443 & 22). Later we will show how to access this dashboard without opening any other ports.

Create the web network

You only need to do this once

docker network create web

Start the traefik container

docker compose up -d

Project Setup

mkdir /data/sites/my-project

Add the compose.yaml

# /data/sites/my-project/compose.yml

name: ${APP_NAME}_prod

services:
  caddy:
    restart: unless-stopped
    image: ${CI_REGISTRY_IMAGE}:caddy-${RELEASE}
    #ports:
    #  - 80:80
    #  - 443:443
    networks:
      - web
      - internal
    volumes:
      - caddy_data:/data
      - caddy_config:/config
      - app_public_storage:/app/public/uploads
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.${APP_NAME}.rule=Host(`my-project.com`) || Host(`www.my-project.com`)"
      - "traefik.http.routers.${APP_NAME}.entrypoints=websecure"
      - "traefik.http.routers.${APP_NAME}.tls.certresolver=le"
      - "traefik.http.services.${APP_NAME}.loadbalancer.server.port=80"

  php:
    restart: unless-stopped
    image: ${CI_REGISTRY_IMAGE}:app-${RELEASE}

    networks:
      - internal
    volumes:
      - app_public_storage:/app/public/uploads
      - app_private_storage:/app/storage

volumes:
  caddy_data:
  caddy_config:
  app_public_storage:
  app_private_storage:

networks:
  web:
    external: true
  internal:

Add the .env file

# /data/sites/my-project/.env

APP_NAME=my-project
CI_REGISTRY_IMAGE=registry.gitlab.com/[USERNAME]/[REPOSITORY]
RELEASE=v1.6.0

Create a PAT so your server can retrieve the containers

Go to https://gitlab.com/-/user_settings/personal_access_tokens and create a personal access token to pull the containers. The PAT only needs the read_registry scope and if you are running a multi project server, you should only need a single PAT for all projects. (only need to set this up once).

Log in to the registry with your PAT

docker login https://registry.gitlab.com -u [USERNAME]

Point the DNS to the new server

??

Clear the cache

docker compose exec php bash -c "./bin/console cache:clear"

Start the project

docker compose up