Docker

Deployment Guide

This guide covers deploying Team Server using Docker and Docker Compose. Choose the method that best fits your infrastructure and requirements.

Prerequisites

Before starting, ensure you have:

  • Docker Engine 24.0+ installed and running
  • Docker Compose v2.0+ (for Docker Compose deployment)
  • PostgreSQL database (local or remote)
  • Team Server configuration file (production.yaml)
  • At least 2GB of available RAM
  • 10GB of available disk space

Method 1: Docker Compose (Recommended)

Docker Compose provides the easiest way to deploy Team Server with all its dependencies.

Step 1: Create Project Directory

mkdir team-server
cd team-server

Step 2: Create Configuration Directory

Create the configuration directory and file:

mkdir config

Create config/production.yaml with your Team Server configuration. For more information about the configuration file, visit the comprehensive documentation here.

Note: The configuration file uses environment variable templating (e.g., {{ get_env(name="VAR_NAME") }}). Set these environment variables before starting the container.

# Minimal example configuration
logger:
  enable: true
  level: info
  format: compact

scheduler:
  output: stdout
  jobs:
    calculate_statistics:
      run: "calculate_statistics"
      schedule: "0 0 5 * * * *"
    update_status:
      run: "update_status"
      schedule: "0 */5 * * * * *"

server:
  binding: 0.0.0.0
  port: 5150
  host: "{{ get_env(name='SERVER_URL') }}"
  middlewares:
    compression:
      enable: true
    cors:
      enable: true
      allow_origins:
        - "{{ get_env(name='SERVER_URL') }}"
      allow_headers:
        - "*"
      allow_methods:
        - "*"
    fallback:
      enable: false
    limit_payload:
      body_limit: 5mb
    logger:
      enable: true
    secure_headers:
      preset: github
    static:
      enable: true
      must_exist: false
      folder:
        uri: "/"
        path: "public"
      fallback: "public/index.html"

database:
  uri: "{{ get_env(name='DATABASE_URL') }}"
  auto_migrate: true
  connect_timeout: 1500
  enable_logging: false
  idle_timeout: 500
  max_connections: 2
  min_connections: 2

workers:
  mode: BackgroundAsync

settings:
  jwt:
    sensor:
      secret: "{{ get_env(name='JWT_SENSOR_SECRET') }}"
      expiration: 31557600 # 1 year in seconds
    user:
      secret: "{{ get_env(name='JWT_USER_SECRET') }}"
      expiration: 604800 # 7 days in seconds
  tenant:
    name: "{{ get_env(name='TENANT_NAME') }}"
    base_url: "{{ get_env(name='SERVER_URL') }}"

# TODO: configure your preferred authentication provider

Finally, update the production.yaml configuration file to reflect your preferred authentication provider.

Step 3: Create Environment File

Create a .env file with your secrets (never commit this file to version control):

Generate .env with secrets:

PGDB=team_server
PGPASS=$(openssl rand -hex 16)
PGUSER=team_server
cat > .env <<EOF
POSTGRES_DB=$PGDB
POSTGRES_PASSWORD=$PGPASS
POSTGRES_USER=$PGUSER
DATABASE_URL=postgresql://$PGUSER:$PGPASS@postgres:5432/$PGDB
JWT_SENSOR_SECRET=$(openssl rand -hex 32)
JWT_USER_SECRET=$(openssl rand -hex 32)
EOF

Then update the .env file to include SERVER_URL and TENANT_NAME:

# .env file - keep this secure and never commit to git
POSTGRES_DB=team_server
POSTGRES_PASSWORD=[redacted]
POSTGRES_USER=team_server
DATABASE_URL=postgresql://team_server:[redacted]@postgres:5432/team_server
JWT_SENSOR_SECRET=[redacted]
JWT_USER_SECRET=[redacted]
SERVER_URL=http://localhost:5150
TENANT_NAME=<your_organization_name>

Step 4: Create Docker Compose Configuration

Create a docker-compose.yaml file:

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  endura-team-server:
    image: ghcr.io/endurasecurity/container/endura-team-server:testing
    ports:
      - "5150:5150"
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SENSOR_SECRET=${JWT_SENSOR_SECRET}
      - JWT_USER_SECRET=${JWT_USER_SECRET}
      - SERVER_URL=${SERVER_URL}
      - TENANT_NAME=${TENANT_NAME}
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "${SERVER_URL}/_readiness"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    volumes:
      - ./config:/app/config

volumes:
  postgres_data:

Important Security Notes:

  • Never commit the .env file to version control (add it to .gitignore)
  • Generate secure, random values for all secrets (minimum 32 characters)
  • For production deployments, use Docker secrets or external secret management
  • Ensure the database password in .env matches between POSTGRES_PASSWORD and DATABASE_URL

Step 5: Deploy Team Server

# Start all services
docker compose up -d

# View logs to monitor startup
docker compose logs -f endura-team-server

Step 6: Verify Deployment

Check that all services are running:

# Check service status
docker compose ps

# Verify Team Server health
curl http://localhost:5150/_readiness

# Check application logs
docker compose logs endura-team-server

You should see:

  • All services in “Up” status
  • Health checks passing with a 200 status and body of {“ok”:true}
  • No error messages in logs

Method 2: Direct Docker Deployment

For deployments where you manage PostgreSQL separately.

Prerequisites

Before proceeding, ensure you have a PostgreSQL instance running with a dedicated database for Team Server. The steps below assume the following setup:

  • PostgreSQL is running in a container named postgres on port 5432
  • A user named team_server exists with full read/write access to a database also named team_server

Adjust the commands as needed to match your environment.

Step 1: Create Project Directory

mkdir team-server
cd team-server

Step 2: Create Configuration Directory

Create the configuration directory and file:

mkdir config

Create config/production.yaml with your Team Server configuration. For more information about the configuration file, visit the comprehensive documentation here.

Note: The configuration file uses environment variable templating (e.g., {{ get_env(name="VAR_NAME") }}). Set these environment variables before starting the container.

# Minimal example configuration
logger:
  enable: true
  level: info
  format: compact

scheduler:
  output: stdout
  jobs:
    calculate_statistics:
      run: "calculate_statistics"
      schedule: "0 0 5 * * * *"
    update_status:
      run: "update_status"
      schedule: "0 */5 * * * * *"

server:
  binding: 0.0.0.0
  port: 5150
  host: "{{ get_env(name='SERVER_URL') }}"
  middlewares:
    compression:
      enable: true
    cors:
      enable: true
      allow_origins:
        - "{{ get_env(name='SERVER_URL') }}"
      allow_headers:
        - "*"
      allow_methods:
        - "*"
    fallback:
      enable: false
    limit_payload:
      body_limit: 5mb
    logger:
      enable: true
    secure_headers:
      preset: github
    static:
      enable: true
      must_exist: false
      folder:
        uri: "/"
        path: "public"
      fallback: "public/index.html"

database:
  uri: "{{ get_env(name='DATABASE_URL') }}"
  auto_migrate: true
  connect_timeout: 1500
  enable_logging: false
  idle_timeout: 500
  max_connections: 2
  min_connections: 2

workers:
  mode: BackgroundAsync

settings:
  jwt:
    sensor:
      secret: "{{ get_env(name='JWT_SENSOR_SECRET') }}"
      expiration: 31557600 # 1 year in seconds
    user:
      secret: "{{ get_env(name='JWT_USER_SECRET') }}"
      expiration: 604800 # 7 days in seconds
  tenant:
    name: "{{ get_env(name='TENANT_NAME') }}"
    base_url: "{{ get_env(name='SERVER_URL') }}"

# TODO: configure your preferred authentication provider

Finally, update the production.yaml configuration file to reflect your preferred authentication provider.

Step 3: Create Environment File

Create a .env file with your secrets (never commit this file to version control):

Generate .env with secrets:

PGDB=team_server
PGPASS=$(openssl rand -hex 16)
PGUSER=team_server
cat > .env <<EOF
POSTGRES_DB=$PGDB
POSTGRES_PASSWORD=$PGPASS
POSTGRES_USER=$PGUSER
DATABASE_URL=postgresql://$PGUSER:$PGPASS@postgres:5432/$PGDB
JWT_SENSOR_SECRET=$(openssl rand -hex 32)
JWT_USER_SECRET=$(openssl rand -hex 32)
EOF

Then update the .env file to include SERVER_URL and TENANT_NAME:

# .env file - keep this secure and never commit to git
POSTGRES_DB=team_server
POSTGRES_PASSWORD=[redacted]
POSTGRES_USER=team_server
DATABASE_URL=postgresql://team_server:[redacted]@postgres:5432/team_server
JWT_SENSOR_SECRET=[redacted]
JWT_USER_SECRET=[redacted]
SERVER_URL=http://localhost:5150
TENANT_NAME=<your_organization_name>

Step 3: Create Docker Network (Optional)

Run the following command to create a network shared between PostgreSQL and Team Server:

docker network create endura-team-server

Note: This is optional and only required if PostgreSQL and Team Server will communicate between containers on a shared host.

Step 4: Run PostgreSQL Container (Optional)

Create a directory to store your PostgreSQL data and serve as a Docker volume:

mkdir data

Run your preferred PostgreSQL container similar to the following command, ensuring you specify --env-file for secrets access and -v for mounting the data volume.

docker run -d \
  --name postgres \
  --network endura-team-server \
  --env-file .env \
  -p 5432:5432 \
  -v data:/var/lib/postgresql/data \
  --restart unless-stopped \
  postgres:16-alpine

Note: This is optional and only required if PostgreSQL is to be run as a stand-alone container.

Step 5: Run Team Server Container

Run the Team Server container similar to the following command, ensuring you specify --env-file for secrets access and -v for mounting the configuration volume.

docker run -d \
  --name endura-team-server \
  --network endura-team-server \
  --user $(id -u):$(id -g) \
  --env-file .env \
  -p 5150:5150 \
  -v ./config:/app/config \
  --restart unless-stopped \
  ghcr.io/endurasecurity/container/endura-team-server:testing

Step 6: Verify Deployment

Check that all services are running:

# Check service status
docker ps

# Verify Team Server health
curl http://localhost:5150/_readiness

# Check application logs
docker logs endura-team-server

You should see:

  • All services in “Up” status
  • Health checks passing with a 200 status and body of {“ok”:true}
  • No error messages in logs

Systemd Service Configuration

To ensure Team Server starts automatically on system boot and can be managed using standard systemd commands, you can create a systemd service file.

Docker Compose Method

Create a systemd service file at /etc/systemd/system/endura-team-server.service:

[Unit]
Description=Endura Team Server (Docker Compose)
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/path/to/team-server
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

Replace /path/to/team-server with the absolute path to your project directory containing the docker-compose.yaml file.

Enable and start the service:

# Reload systemd to recognize the new service
sudo systemctl daemon-reload

# Enable the service to start on boot
sudo systemctl enable endura-team-server.service

# Start the service
sudo systemctl start endura-team-server.service

# Check service status
sudo systemctl status endura-team-server.service

Direct Docker Method

Create a systemd service file at /etc/systemd/system/endura-team-server.service:

[Unit]
Description=Endura Team Server (Docker)
Requires=docker.service
After=docker.service

[Service]
Type=simple
Restart=always
RestartSec=5
WorkingDirectory=/path/to/team-server
ExecStartPre=-/usr/bin/docker rm -f endura-team-server
ExecStart=/usr/bin/docker run --rm \
  --name endura-team-server \
  --network endura-team-server \
  --user %U:%G \
  --env-file /path/to/team-server/.env \
  -p 5150:5150 \
  -v /path/to/team-server/config:/app/config \
  ghcr.io/endurasecurity/container/endura-team-server:testing
ExecStop=/usr/bin/docker stop endura-team-server

[Install]
WantedBy=multi-user.target

Replace /path/to/team-server with the absolute path to your project directory. Add any additional volume mounts (such as TLS certificates) as needed.

Enable and start the service:

# Reload systemd to recognize the new service
sudo systemctl daemon-reload

# Enable the service to start on boot
sudo systemctl enable endura-team-server.service

# Start the service
sudo systemctl start endura-team-server.service

# Check service status
sudo systemctl status endura-team-server.service

Managing the Service

Common systemd commands for managing the service:

# View service logs
sudo journalctl -u endura-team-server.service -f

# Restart the service
sudo systemctl restart endura-team-server.service

# Stop the service
sudo systemctl stop endura-team-server.service

# Disable auto-start on boot
sudo systemctl disable endura-team-server.service

Set Up Your First Administrator

When users first access Team Server, they are automatically assigned the Viewer role with read-only access. To manage Team Server, you need to promote at least one user to the Administrator role.

User Must Log In First

The user must access Team Server and complete authentication before you can change their role. This creates their user record in the database. If you run these commands before the user has logged in, they will fail because the email address will not be found.

Step 1: Log In to Team Server

Open your browser and navigate to your Team Server URL. Complete the authentication process using your configured OIDC provider. This creates your user record in the database.

Step 2: Get the User ID

Run the following command, replacing the email address with your own:

docker compose exec endura-team-server endura task user_get_id email:your-email@example.com
docker exec endura-team-server endura task user_get_id email:your-email@example.com

This command outputs a numeric user ID.

Step 3: Assign the Administrator Role

Run the following command, replacing <user_id> with the numeric ID from the previous step:

docker compose exec endura-team-server endura task user_set_role id:<user_id> role:administrator
docker exec endura-team-server endura task user_set_role id:<user_id> role:administrator

Step 4: Verify the Role Change

Refresh your browser or log out and log back in to Team Server. You should now see an Administration menu item in the main navigation, confirming your Administrator role is active.

TLS Configuration

Team Server supports TLS termination directly within the application. This method provides end-to-end encryption without requiring external TLS proxies.

When to Use TLS

Use TLS configuration when:

  • You need end-to-end encryption for production deployments
  • You want to avoid external load balancers or reverse proxies
  • You have your own SSL certificates to use
  • You’re deploying in environments where external TLS termination isn’t available

For development: You can use self-signed certificates for testing, but browsers will show security warnings.

Step 1: Prepare TLS Certificates

Create or obtain your TLS certificate files:

# Create a certificates directory
mkdir certs

# Example: Create self-signed certificates for testing
openssl req -x509 \
  -newkey rsa:4096 \
  -keyout certs/server-key.pem \
  -out certs/server.pem \
  -days 365 \
  -nodes \
  -subj "/C=US/ST=State/L=City/O=MyOrg/CN=team-server.your-domain.com"

Step 2: Update Configuration File

Update settings to contain a tls section in your config/production.yaml file. Secrets are mounted at /run/secrets/ inside the container:

settings:
  tls:
    certificate: "/run/secrets/server_cert"
    private_key: "/run/secrets/server_key"

Step 3: Update Environment File

Update your .env file to use HTTPS and add user ID variables:

# Add HTTPS URL
echo "SERVER_URL=https://localhost:5150" >> .env

# Add user/group IDs (required for secret file permissions)
echo "UID=$(id -u)" >> .env
echo "GID=$(id -g)" >> .env

Step 4: Configure TLS with Secrets

Docker Compose Method

Update your docker-compose.yaml to include secrets:

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  endura-team-server:
    image: ghcr.io/endurasecurity/container/endura-team-server:testing
    user: "${UID}:${GID}"
    ports:
      - "5150:5150"
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SENSOR_SECRET=${JWT_SENSOR_SECRET}
      - JWT_USER_SECRET=${JWT_USER_SECRET}
      - SERVER_URL=${SERVER_URL}
      - TENANT_NAME=${TENANT_NAME}
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "-k", "${SERVER_URL}/_readiness"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    volumes:
      - ./config:/app/config
    secrets:
      - server_cert
      - server_key

secrets:
  server_cert:
    file: ./certs/server.pem
  server_key:
    file: ./certs/server-key.pem

volumes:
  postgres_data:

Then restart the services:

docker compose down
docker compose up -d

Direct Docker Method

For standalone Docker without Swarm, mount the certificate files directly to the secrets path:

# Stop and remove existing container
docker rm --force endura-team-server

# Run with certificate files mounted as secrets
docker run -d \
  --name endura-team-server \
  --network endura-team-server \
  --user $(id -u):$(id -g) \
  --env-file .env \
  -p 5150:5150 \
  -v ./config:/app/config \
  -v ./certs/server.pem:/run/secrets/server_cert:ro \
  -v ./certs/server-key.pem:/run/secrets/server_key:ro \
  --restart unless-stopped \
  ghcr.io/endurasecurity/container/endura-team-server:testing

The --user flag runs the container as your host user, allowing access to the certificate files. The :ro flag mounts the certificates as read-only for additional security.

Step 5: Verify TLS Configuration

# Verify TLS is working (note the https:// and -k flag for self-signed certs)
curl -k https://localhost:5150/_readiness

# Check application logs for TLS startup messages
docker compose logs endura-team-server | grep -i tls

# For direct container
docker logs endura-team-server | grep -i tls

Security Notes for Production:

  • Use certificates from a trusted Certificate Authority
  • Store certificate files securely and limit file permissions
  • Docker Compose secrets are mounted with secure permissions automatically
  • Regularly rotate certificates before expiration
  • Use strong cipher suites and TLS 1.2+ only

Updating Team Server

Docker Compose Method

# Stop current deployment
docker compose down

# Pull specific version by updating the image tag first
docker compose pull ghcr.io/endurasecurity/container/endura-team-server:testing

# Start with new image
docker compose up -d

# Verify update
docker compose logs endura-team-server

Direct Docker Method

# Stop current container
docker rm --force endura-team-server

# Pull specific version
docker pull ghcr.io/endurasecurity/container/endura-team-server:testing

# Run with new image (use same command as initial deployment)
docker run -d \
  --name endura-team-server \
  [... same parameters as before ...]
  ghcr.io/endurasecurity/container/endura-team-server:testing

Backup and Restore

Backup

# Backup PostgreSQL database via Docker Compose
docker compose exec postgres pg_dump -U team_server team_server > team_server_backup.sql

# Backup PostgreSQL database via Direct Docker
docker exec postgres pg_dump -U team_server team_server > team_server_backup.sql

# Backup configuration files
tar czf team_server_config_backup.tar.gz config/

Restore

# Restore PostgreSQL database via Docker Compose
docker compose exec -T postgres psql -U team_server team_server < team_server_backup.sql

# Restore PostgreSQL database via Direct Docker
docker exec postgres psql -U team_server team_server < team_server_backup.sql

# Restore configuration files
tar xzf team_server_config_backup.tar.gz

Uninstalling Team Server

Docker Compose Method

# Stop and remove all services
docker compose down

# Remove volumes (WARNING: This deletes all data)
docker compose down -v

# Remove images
docker rmi ghcr.io/endurasecurity/container/endura-team-server:testing postgres:16

# Clean up unused resources
docker system prune -f

Direct Docker Method

# Stop and remove endura-team-server
docker rm --force endura-team-server

# (Optional) Stop and remove postgres
docker rm --force postgres

# Remove network
docker network rm endura-team-server

# Remove image
docker rmi ghcr.io/endurasecurity/container/endura-team-server:testing

# Clean up unused resources
docker system prune -f

Troubleshooting

Common Issues

Container fails to start:

# Check logs for Docker Compose
docker compose logs endura-team-server

# Check logs for Direct Docker
docker logs endura-team-server

# Verify configuration
docker compose config

# Check environment variables (be careful not to expose secrets in logs)
docker compose exec endura-team-server env | grep -E '(SERVER_URL|TENANT_NAME)'

Database connection issues:

When using Docker Compose:

# Check PostgreSQL logs
docker compose logs postgres

When using Direct Docker:

# Check PostgreSQL logs
docker logs postgres

Permission issues:

# Check configuration file permissions
ls -la config/

# Fix ownership if needed
sudo chown -R $(id -u):$(id -g) config/
chmod -R ug+rx config/

Memory issues:

# Check memory usage
docker stats

# Check Docker resource limits
docker inspect endura-team-server | grep -i memory

Getting Help

If you encounter issues:

  1. Check the logs: docker compose logs endura-team-server
  2. Verify all environment variables and configuration files are set correctly
  3. Ensure PostgreSQL is healthy and configuration file is mounted correctly
  4. Check network connectivity between containers
  5. Verify container image accessibility

For additional support, contact us at support@endurasecurity.com