Podman

Deployment Guide

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

Prerequisites

Before starting, ensure you have:

  • Podman 4.9+ installed and running
  • Podman Compose v1.0+ (for Podman Compose deployment)
  • For rootless mode: proper user namespace configuration and loginctl linger enabled
  • For SELinux systems: container_manage_cgroup boolean enabled
  • PostgreSQL database (local or remote)
  • Team Server configuration file (production.yaml)
  • At least 2GB of available RAM
  • 10GB of available disk space

Method 1: Podman Compose (Recommended)

Podman 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 Podman Compose Configuration

Create a podman-compose.yaml file:

services:
  postgres:
    image: docker.io/postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data:Z
    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
    userns_mode: "keep-id"
    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:z

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 Podman secrets, Kubernetes secrets, or external secret management
  • Ensure the database password in .env matches between POSTGRES_PASSWORD and DATABASE_URL
  • Volume mounts use SELinux labels: :z for shared content, :Z for private content

Step 5: Deploy Team Server

# Start all services
podman compose up -d

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

Step 6: Verify Deployment

Check that all services are running:

# Check service status
podman compose ps

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

# Check application logs
podman 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 Podman 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 Podman Network (Optional)

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

podman 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 Podman 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.

podman run -d \
  --name postgres \
	--network endura-team-server \
  --env-file .env \
	-p 5432:5432 \
  -v data:/var/lib/postgresql/data \
  --restart unless-stopped \
	docker.io/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.

podman run -d \
  --name endura-team-server \
  --network endura-team-server \
  --userns=keep-id \
  --env-file .env \
  -p 5150:5150 \
  -v ./config:/app/config:z \
  --restart unless-stopped \
  ghcr.io/endurasecurity/container/endura-team-server:testing

Step 6: Verify Deployment

Check that all services are running:

# Check service status
podman ps

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

# Check application logs
podman 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. Podman integrates well with systemd and can generate service files automatically.

Prerequisites for Rootless Podman

For rootless Podman containers to persist after logout, enable loginctl linger:

# Enable linger for your user
loginctl enable-linger $USER

# Verify linger is enabled
loginctl show-user $USER | grep Linger

Podman Compose Method

Create a systemd user service file at ~/.config/systemd/user/endura-team-server.service:

mkdir -p ~/.config/systemd/user
[Unit]
Description=Endura Team Server (Podman Compose)
Wants=network-online.target
After=network-online.target

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

[Install]
WantedBy=default.target

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

Enable and start the service:

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

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

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

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

Direct Podman Method

Podman can generate systemd service files automatically from running containers:

# Ensure the container is running first
podman ps

# Generate a systemd user service file
mkdir -p ~/.config/systemd/user
podman generate systemd --name endura-team-server --files --new
mv container-endura-team-server.service ~/.config/systemd/user/endura-team-server.service

The --new flag creates a service that recreates the container on each start, which is recommended for updates.

Alternatively, create the service file manually at ~/.config/systemd/user/endura-team-server.service:

[Unit]
Description=Endura Team Server (Podman)
Wants=network-online.target
After=network-online.target

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

[Install]
WantedBy=default.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
systemctl --user daemon-reload

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

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

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

Managing the Service

Common systemd commands for managing the service:

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

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

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

# Disable auto-start on boot
systemctl --user 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:

podman compose exec endura-team-server endura task user_get_id email:your-email@example.com
podman 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:

podman compose exec endura-team-server endura task user_set_role id:<user_id> role:administrator
podman 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"

# Ensure your user can read the certificate files
chown -R $(id -u):$(id -g) certs
chmod -R ug+r certs

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 SERVER_URL in your .env file to use HTTPS:

SERVER_URL=https://localhost:5150

Step 4: Configure TLS with Secrets

Podman Compose Method

Update your podman-compose.yaml to include secrets:

services:
  postgres:
    image: docker.io/postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data:Z
    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
    userns_mode: "keep-id"
    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:z
      - ./certs/server.pem:/run/secrets/server_cert:ro,z
      - ./certs/server-key.pem:/run/secrets/server_key:ro,z

volumes:
  postgres_data:

Then restart the services:

podman compose down
podman compose up -d

Direct Podman Method

For standalone Podman deployment, mount the certificate files directly to the secrets path:

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

# Run with certificate files mounted as secrets
podman run -d \
  --name endura-team-server \
  --network endura-team-server \
  --userns=keep-id \
  --env-file .env \
  -p 5150:5150 \
  -v ./config:/app/config:z \
  -v ./certs/server.pem:/run/secrets/server_cert:ro,z \
  -v ./certs/server-key.pem:/run/secrets/server_key:ro,z \
  --restart unless-stopped \
  ghcr.io/endurasecurity/container/endura-team-server:testing

The --userns=keep-id flag maps your host user directly into the container, allowing access to the certificate files with standard Unix permissions. 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
podman compose logs endura-team-server | grep -i tls

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

# Verify secrets are mounted correctly
podman exec endura-team-server ls -la /run/secrets/

Security Notes for Production:

  • Use certificates from a trusted Certificate Authority
  • Store certificate files securely and limit file permissions
  • Regularly rotate certificates before expiration
  • Use strong cipher suites and TLS 1.2+ only

Updating Team Server

Podman Compose Method

# Stop current deployment
podman compose down

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

# Start with new image
podman compose up -d

# Verify update
podman compose logs endura-team-server

Direct Podman Method

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

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

# Run with new image (use same command as initial deployment)
podman 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 Podman Compose
podman compose exec postgres pg_dump -U team_server team_server > team_server_backup.sql

# Backup PostgreSQL database via Direct Podman
podman 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 Podman Compose
podman compose exec -T postgres psql -U team_server team_server < team_server_backup.sql

# Restore PostgreSQL database via Direct Podman
podman 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

Podman Compose Method

# Stop and remove all services
podman compose down

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

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

# Clean up unused resources
podman system prune -f

Direct Podman Method

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

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

# Remove network
podman network rm endura-team-server

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

# Clean up unused resources
podman system prune -f

Troubleshooting

Common Issues

Container fails to start:

# Check logs for Podman Compose
podman compose logs endura-team-server

# Check logs for Direct Podman
podman logs endura-team-server

# Verify configuration
podman compose config

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

Database connection issues:

When using Podman Compose:

# Check PostgreSQL logs
podman compose logs postgres

When using Direct Podman:

# Check PostgreSQL logs
podman 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/

Rootless Podman issues:

# Check if running rootless
podman info | grep -i rootless

# Verify user namespace mapping
podman unshare cat /proc/self/uid_map

# Check for SELinux issues (if applicable)
sudo sealert -a /var/log/audit/audit.log

# Ensure proper cgroup configuration
sudo setsebool -P container_manage_cgroup true

# Check if loginctl linger is enabled for persistent services
loginctl show-user $USER | grep Linger

Memory issues:

# Check memory usage
podman stats

# Check systemd resource limits for rootless mode
systemctl --user show user.slice | grep Memory

# For rootless mode, check if cgroup v2 is enabled
cat /sys/fs/cgroup/cgroup.controllers

Getting Help

If you encounter issues:

  1. Check the logs: podman 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
  6. For rootless issues, check user namespace, SELinux configuration, and cgroup settings
  7. Ensure volume mounts use proper SELinux labels (:z for shared, :Z for private)
  8. Verify loginctl linger is enabled for persistent rootless containers

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