Skip to the content.

Deploy qhook to AWS

Prerequisites


1.1 Bake Config into Docker Image

Create a project-specific Dockerfile that embeds your qhook.yaml into the image. Secrets (DB credentials, webhook secrets) should use ${VAR} placeholders — qhook expands environment variables at startup.

your-project/
├── Dockerfile
└── qhook.yaml       # config with ${VAR} placeholders for secrets

qhook.yaml example:

server:
  port: 8888

database:
  url: "${DATABASE_URL}"

sources:
  stripe:
    type: webhook
    path: /webhook/stripe
    verify: stripe
    secret: "${STRIPE_WEBHOOK_SECRET}"
    handlers:
      - url: http://billing-service:3000/stripe

Dockerfile:

FROM ghcr.io/totte-dev/qhook:latest
COPY qhook.yaml /data/qhook.yaml

Build and push:

aws ecr create-repository --repository-name qhook

AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
AWS_REGION=ap-northeast-1

aws ecr get-login-password --region $AWS_REGION | \
  docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com

docker build -t $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/qhook:latest .
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/qhook:latest

Config changes = image rebuild. This is intentional — config is part of your deployment artifact, versioned and reproducible. Secrets stay in environment variables / Secrets Manager.

1.2 Task Definition

{
  "family": "qhook",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
  "containerDefinitions": [
    {
      "name": "qhook",
      "image": "ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/qhook:latest",
      "portMappings": [
        {
          "containerPort": 8888,
          "protocol": "tcp"
        }
      ],
      "environment": [
        { "name": "RUST_LOG", "value": "qhook=info" }
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:ACCOUNT_ID:secret:qhook/database-url"
        },
        {
          "name": "STRIPE_WEBHOOK_SECRET",
          "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:ACCOUNT_ID:secret:qhook/stripe-webhook-secret"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/qhook",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "qhook"
        }
      },
      "essential": true
    }
  ]
}

No volumes or EFS needed. Config is baked into the image; secrets are injected via Secrets Manager at runtime.

1.3 Create Service (with ALB)

# Create target group (port 8888, health check /health)
aws elbv2 create-target-group \
  --name qhook-tg \
  --protocol HTTP \
  --port 8888 \
  --vpc-id vpc-xxxxxxxx \
  --target-type ip \
  --health-check-path /health

# Create ECS service
aws ecs create-service \
  --cluster default \
  --service-name qhook \
  --task-definition qhook \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-aaa,subnet-bbb],securityGroups=[sg-xxx],assignPublicIp=DISABLED}" \
  --load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/qhook-tg/xxx,containerName=qhook,containerPort=8888"

1.4 RDS PostgreSQL

aws secretsmanager create-secret \
  --name qhook/database-url \
  --secret-string "postgres://qhook:password@rds-host:5432/qhook"

1.5 Environment Variables

Variable Description Example
DATABASE_URL DB connection string (via Secrets Manager) postgres://user:pass@host:5432/qhook
RUST_LOG Log level qhook=info
STRIPE_WEBHOOK_SECRET Webhook secret (via Secrets Manager) whsec_...
QHOOK_CONFIG Config file path (default: /data/qhook.yaml) /data/qhook.yaml

2. EC2 (Single Instance)

For small-scale or test environments.

2.1 Docker + docker-compose

docker-compose.prod.yaml:

services:
  qhook:
    image: qhook:latest
    restart: always
    ports:
      - "127.0.0.1:8888:8888"
    volumes:
      - ./qhook.yaml:/data/qhook.yaml:ro
      - qhook-data:/data/db
    environment:
      - RUST_LOG=qhook=info
      - DATABASE_URL=postgres://qhook:password@db:5432/qhook

  db:
    image: postgres:16-alpine
    restart: always
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=qhook
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=qhook

volumes:
  qhook-data:
  pgdata:
docker compose -f docker-compose.prod.yaml up -d

2.2 systemd Service

For running the binary directly without Docker Compose.

/etc/systemd/system/qhook.service:

[Unit]
Description=qhook event gateway
After=network.target postgresql.service

[Service]
Type=simple
User=qhook
Group=qhook
ExecStart=/usr/local/bin/qhook start --config /data/qhook.yaml
Restart=always
RestartSec=5
Environment=RUST_LOG=qhook=info
Environment=DATABASE_URL=postgres://qhook:password@localhost:5432/qhook
WorkingDirectory=/data
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now qhook

2.3 Nginx Reverse Proxy

/etc/nginx/sites-available/qhook:

server {
    listen 80;
    server_name webhook.example.com;

    # Redirect to HTTPS (after obtaining cert via Let's Encrypt)
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name webhook.example.com;

    ssl_certificate     /etc/letsencrypt/live/webhook.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/webhook.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8888;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Webhook payloads can be large
        client_max_body_size 10m;
    }
}
sudo ln -s /etc/nginx/sites-available/qhook /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

qhook is not suitable for serverless environments:

A split architecture (Lambda for receiving, separate service for queue processing) is possible but goes against qhook’s design philosophy of a single self-contained binary.

Conclusion: Run qhook as a long-running process on ECS Fargate or EC2.