@ossy/deployment-tools 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 1.21.0 (2026-05-26)
7
+
8
+
9
+ ### Features
10
+
11
+ * **business-strategy:** Add comprehensive business strategy document ([408a820](https://github.com/ossy-se/ossy/commit/408a820e7b86019faba178596cc759bd88b4a620))
12
+
13
+
14
+
15
+
16
+
17
+ ## 1.20.1 (2026-05-26)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **deployment-tools:** Update Caddy service setup and Ubuntu AMI version ([6b3f4f7](https://github.com/ossy-se/ossy/commit/6b3f4f755901450be744d4402ec1f314418d101e))
23
+
24
+
25
+
26
+
27
+
6
28
  # 1.20.0 (2026-05-25)
7
29
 
8
30
 
package/README.md CHANGED
@@ -49,7 +49,11 @@ Additional services are generated automatically from the `services` array in `pl
49
49
 
50
50
  ### `services` — container/domain mapping
51
51
 
52
- Each entry in `services` generates a systemd unit that pulls and runs a Docker image, and a Caddy block that routes a domain to it. All variables from `env` are forwarded to the container via `--env-file /etc/environment`.
52
+ Services come in two flavours: **HTTP** (routed through Caddy) and **TCP/UDP** (raw socket, bypasses Caddy). Both generate a systemd unit that pulls the image on every restart and forwards `/etc/environment` to the container.
53
+
54
+ #### HTTP service (default)
55
+
56
+ Generates a Caddy reverse-proxy block with Route53 TLS. The container must listen on port 3000.
53
57
 
54
58
  ```json
55
59
  "services": [
@@ -63,11 +67,35 @@ Each entry in `services` generates a systemd unit that pulls and runs a Docker i
63
67
 
64
68
  | Field | Description |
65
69
  |---|---|
66
- | `name` | Unique service name. Used as the Docker container name and systemd unit name (`<name>.service`). |
67
- | `domain` | Domain this service handles. Gets its own Caddy block with Route53 TLS. |
70
+ | `name` | Unique service name Docker container name and systemd unit (`<name>.service`). |
71
+ | `domain` | Hostname routed by Caddy to this container (Route53 TLS via `dns route53`). |
68
72
  | `image` | Docker image to pull and run. |
69
73
 
70
- Host ports are auto-assigned starting at `3002` based on the order in the array (`services[0]` `3002`, `services[1]` → `3003`, etc.). Appending new entries is safe; avoid reordering existing ones.
74
+ Host ports are auto-assigned starting at `3002`, counting only HTTP services (TCP entries are skipped). Appending new HTTP entries is safe; avoid reordering existing ones.
75
+
76
+ #### TCP/UDP service
77
+
78
+ No Caddy block. Ports are mapped directly (`HOST:CONTAINER`) and opened in the EC2 security group. Use this for game servers or any raw socket protocol.
79
+
80
+ ```json
81
+ "services": [
82
+ {
83
+ "name": "minecraft",
84
+ "type": "tcp",
85
+ "image": "itzg/minecraft-server",
86
+ "ports": [25565],
87
+ "protocol": "tcp"
88
+ }
89
+ ]
90
+ ```
91
+
92
+ | Field | Description |
93
+ |---|---|
94
+ | `name` | Unique service name — Docker container name and systemd unit. |
95
+ | `type` | Must be `"tcp"` to enable this mode. |
96
+ | `image` | Docker image to pull and run. |
97
+ | `ports` | Array of port numbers to expose on the host and open in the security group. |
98
+ | `protocol` | `"tcp"` (default), `"udp"`, or `"both"`. |
71
99
 
72
100
  **Fixed built-in services** (always present, not configurable via `services`):
73
101
  - `ossy-api` — the Ossy API (`ghcr.io/ossy-se/api:latest`, port 3001)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/deployment-tools",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "Collection of scripts and tools to aid deployment of containers and static files to Amazon Web Services through GitHub Actions",
5
5
  "source": "./src/index.js",
6
6
  "main": "./src/index.js",
@@ -22,5 +22,5 @@
22
22
  "devDependencies": {
23
23
  "jest": "^27.5.1"
24
24
  },
25
- "gitHead": "50d7e273859b6b77606d8530913b971c7445a34a"
25
+ "gitHead": "6eb721d6fa63b333f4b8f8e9b4178f721ecaa531"
26
26
  }
@@ -5,7 +5,20 @@
5
5
  * @property {string[]=} sesDomains - list of domains to configure AWS SES email identity and DKIM records for
6
6
  * @property {string[]=} domains - list of domains to create Route53 A records for, pointing to the EC2 host
7
7
  * @property {object} dnsRecords - map of dns records by root domain, only supports MX records so that we can add dns records for our email service
8
- * @property {Array<{name: string, domain: string, image: string}>=} services - per-platform container services; each entry generates a systemd unit and a Caddy domain block. Host ports are auto-assigned starting at 3002.
8
+ * @property {Array<HttpServiceConfig|TcpServiceConfig>=} services - per-platform container services; each entry generates a systemd unit. HTTP services also get a Caddy reverse-proxy block. TCP/UDP services get direct port exposure and security group rules instead.
9
+ *
10
+ * @typedef {Object} HttpServiceConfig
11
+ * @property {'http'} [type] - defaults to 'http' when omitted
12
+ * @property {string} name - unique service name (container + systemd unit)
13
+ * @property {string} domain - hostname routed by Caddy to this container
14
+ * @property {string} image - Docker image (e.g. ghcr.io/ossy-se/website-ossy:latest)
15
+ *
16
+ * @typedef {Object} TcpServiceConfig
17
+ * @property {'tcp'} type - marks a raw socket service; bypasses Caddy entirely
18
+ * @property {string} name - unique service name (container + systemd unit)
19
+ * @property {string} image - Docker image (e.g. itzg/minecraft-server)
20
+ * @property {number[]} ports - ports to open on the host and in the EC2 security group
21
+ * @property {'tcp'|'udp'|'both'} [protocol] - defaults to 'tcp'
9
22
  * @property {Record<string,string>=} env - environment variables written to /etc/environment on the EC2 host at boot time; never uploaded to S3
10
23
  *
11
24
  * @property {string} awsAccountId - Aws account id
@@ -21,7 +21,6 @@ api.ossy.se {
21
21
  tls {
22
22
  dns route53 {
23
23
  max_retries 10
24
- profile ci-client
25
24
  }
26
25
  }
27
26
  reverse_proxy localhost:3001
@@ -33,7 +32,6 @@ ${serviceBlocks}
33
32
  on_demand
34
33
  dns route53 {
35
34
  max_retries 10
36
- profile ci-client
37
35
  }
38
36
  }
39
37
  reverse_proxy localhost:3000
@@ -83,7 +81,8 @@ class CaddyService {
83
81
  `sudo tee /etc/caddy/Caddyfile > /dev/null << 'CADDYEOF'\n${caddyfile}\nCADDYEOF`,
84
82
  // Write systemd unit
85
83
  `sudo tee /etc/systemd/system/caddy-route53.service > /dev/null << 'UNITEOF'\n${systemdServiceFile}\nUNITEOF`,
86
- // Install Caddy + xcaddy + Route53 plugin
84
+ // Create caddy system user (apt install caddy may not run on all Ubuntu versions)
85
+ 'sudo useradd --system --home /var/lib/caddy --shell /usr/sbin/nologin --create-home caddy || true',
87
86
  'sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https',
88
87
  'sudo curl -1sLf \'https://dl.cloudsmith.io/public/caddy/stable/gpg.key\' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg',
89
88
  'sudo curl -1sLf \'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt\' | sudo tee /etc/apt/sources.list.d/caddy-stable.list',
@@ -21,8 +21,7 @@ const { getInstallNodeJs, getInstallNpm, getInstallDocker } = require('./user-da
21
21
  const { CaddyService } = require('./caddy.service')
22
22
  const { OssyRuntimeService } = require('./ossy-runtime.service')
23
23
  const { OssyApiService } = require('./ossy-api.service')
24
- const { ContainerService } = require('./container-service')
25
- const { AwsProfile } = require('./aws-profile')
24
+ const { fromConfig: buildContainerServices } = require('./container-service')
26
25
  const { SupportedRegions } = require('../../config')
27
26
 
28
27
  /**
@@ -34,7 +33,7 @@ const { SupportedRegions } = require('../../config')
34
33
  */
35
34
 
36
35
  const InstanceImages = {
37
- UBUNTU: 'ami-092cce4a19b438926'
36
+ UBUNTU: 'ami-067bcf851477ebb78' // Ubuntu Server 24.04 LTS (HVM), eu-north-1
38
37
  }
39
38
 
40
39
  /**
@@ -71,6 +70,16 @@ class ContainerDeploymentTarget extends Construct {
71
70
  'allow HTTPS traffic from anywhere'
72
71
  )
73
72
 
73
+ // Build container services from config — opens additional security group ports for TCP/UDP services
74
+ const containerServices = buildContainerServices(props.config.services)
75
+
76
+ containerServices.forEach(service => {
77
+ service.securityGroupPorts.forEach(({ port, protocol }) => {
78
+ const portRule = protocol === 'udp' ? Port.udp(port) : Port.tcp(port)
79
+ securityGroup.addIngressRule(Peer.anyIpv4(), portRule, `${service.name} ${protocol.toUpperCase()} ${port}`)
80
+ })
81
+ })
82
+
74
83
  const platformConfigDeployment = new BucketDeployment(this, 'PlatformConfigDeployment', {
75
84
  sources: [Source.jsonData('platform-config.json', { ...props.config, env: undefined })],
76
85
  destinationBucket: props.bucket,
@@ -112,9 +121,6 @@ class ContainerDeploymentTarget extends Construct {
112
121
  // Write platform env vars to /etc/environment so all systemd services pick them up.
113
122
  const envLines = Object.entries(props.config.env ?? {}).map(([k, v]) => `${k}=${v}`)
114
123
 
115
- // Build generic container services from config.services
116
- const containerServices = ContainerService.fromConfig(props.config.services)
117
-
118
124
  userData.addCommands(
119
125
  'sudo groupadd docker',
120
126
  'sudo usermod -aG docker ubuntu',
@@ -124,12 +130,11 @@ class ContainerDeploymentTarget extends Construct {
124
130
  ...getInstallNpm(),
125
131
  ...getInstallDocker(),
126
132
  'sudo apt-get install awscli --yes',
127
- ...AwsProfile.writeFile(role.roleArn, SupportedRegions.North),
128
133
  // Write all platform env vars before starting any service
129
134
  `sudo tee /etc/environment << 'ENVEOF'\n${envLines.join('\n')}\nENVEOF`,
130
135
  // Create shared Docker network for inter-container communication
131
136
  'docker network create ossy-network || true',
132
- ...CaddyService.install(containerServices),
137
+ ...CaddyService.install(containerServices.filter(s => s.caddyBlock !== null)),
133
138
  ...OssyRuntimeService.install(),
134
139
  ...OssyApiService.install(),
135
140
  ...containerServices.flatMap(s => s.install())
@@ -1,34 +1,34 @@
1
1
  /**
2
- * ContainerServiceConfig
3
- * @typedef {Object} ContainerServiceConfig
4
- * @property {string} name - Unique service name, used for the container name and systemd unit
5
- * @property {string} domain - Domain this service is reachable on (used in Caddy block)
6
- * @property {string} image - Docker image to run (e.g. ghcr.io/ossy-se/website-ossy:latest)
7
- */
8
-
9
- const BASE_PORT = 3002 // 3000 = platform runtime, 3001 = API
10
-
11
- /**
12
- * ContainerService generates a systemd unit file and a Caddy reverse-proxy block
13
- * for a domain → Docker container mapping defined in platforms.json `services`.
2
+ * @typedef {'http'|'tcp'} ServiceType
3
+ *
4
+ * @typedef {Object} HttpServiceConfig
5
+ * @property {'http'} [type] - defaults to 'http'
6
+ * @property {string} name - unique name, used for container name and systemd unit
7
+ * @property {string} domain - domain this service handles (Caddy block + Route53)
8
+ * @property {string} image - Docker image to run
14
9
  *
15
- * Host ports are auto-assigned starting at 3002 based on the service's index in
16
- * the array. All containers are expected to listen on port 3000 internally.
10
+ * @typedef {Object} TcpServiceConfig
11
+ * @property {'tcp'} type - marks this as a raw TCP/UDP service (no HTTP/Caddy)
12
+ * @property {string} name - unique name, used for container name and systemd unit
13
+ * @property {string} image - Docker image to run
14
+ * @property {number[]} ports - ports to expose directly (same inside and outside container)
15
+ * @property {'tcp'|'udp'|'both'} [protocol] - defaults to 'tcp'
17
16
  *
18
- * All environment variables from /etc/environment are forwarded to the container
19
- * via --env-file, so no per-service env wiring is needed.
17
+ * @typedef {HttpServiceConfig|TcpServiceConfig} ContainerServiceConfig
20
18
  */
21
- class ContainerService {
22
19
 
23
- /**
24
- * @param {ContainerServiceConfig} config
25
- * @param {number} index - position in the services array, used to derive host port
26
- */
27
- constructor({ name, domain, image }, index) {
20
+ const BASE_HTTP_PORT = 3002 // 3000 = platform runtime, 3001 = API
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // HTTP service — reverse-proxied through Caddy, container listens on port 3000
24
+ // ---------------------------------------------------------------------------
25
+
26
+ class HttpContainerService {
27
+ constructor({ name, domain, image }, httpIndex) {
28
28
  this.name = name
29
29
  this.domain = domain
30
30
  this.image = image
31
- this.port = BASE_PORT + index
31
+ this.hostPort = BASE_HTTP_PORT + httpIndex
32
32
  }
33
33
 
34
34
  get systemdUnitFile() {
@@ -44,7 +44,7 @@ ExecStartPre=-/usr/bin/docker rm -f ${this.name}
44
44
  ExecStartPre=/usr/bin/docker pull ${this.image}
45
45
  ExecStart=/usr/bin/docker run --name ${this.name} \\
46
46
  --network ossy-network \\
47
- -p ${this.port}:3000 \\
47
+ -p ${this.hostPort}:3000 \\
48
48
  --env-file /etc/environment \\
49
49
  ${this.image}
50
50
  ExecStop=/usr/bin/docker stop ${this.name}
@@ -62,40 +62,101 @@ ${this.domain} {
62
62
  tls {
63
63
  dns route53 {
64
64
  max_retries 10
65
- profile ci-client
66
65
  }
67
66
  }
68
- reverse_proxy localhost:${this.port}
67
+ reverse_proxy localhost:${this.hostPort}
69
68
  }`
70
69
  }
71
70
 
72
- get unitName() {
73
- return `${this.name}.service`
71
+ /** Ports to open in the EC2 security group. HTTP services use Caddy on 80/443 — no extra rules needed. */
72
+ get securityGroupPorts() {
73
+ return []
74
74
  }
75
75
 
76
- install() {
77
- return [`sudo tee /etc/systemd/system/${this.unitName} > /dev/null << 'EOF'\n${this.systemdUnitFile}\nEOF`]
78
- }
76
+ get unitName() { return `${this.name}.service` }
77
+ install() { return [`sudo tee /etc/systemd/system/${this.unitName} > /dev/null << 'EOF'\n${this.systemdUnitFile}\nEOF`] }
78
+ enable() { return [`sudo systemctl enable ${this.name}`] }
79
+ start() { return [`sudo systemctl start ${this.name}`] }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // TCP/UDP service — bypasses Caddy, ports exposed directly on the host.
84
+ // Used for game servers and any other raw socket services.
85
+ // ---------------------------------------------------------------------------
79
86
 
80
- enable() {
81
- return [`sudo systemctl enable ${this.name}`]
87
+ class TcpContainerService {
88
+ constructor({ name, image, ports, protocol = 'tcp' }) {
89
+ this.name = name
90
+ this.image = image
91
+ this.ports = ports
92
+ this.protocol = protocol // 'tcp' | 'udp' | 'both'
82
93
  }
83
94
 
84
- start() {
85
- return [`sudo systemctl start ${this.name}`]
95
+ get systemdUnitFile() {
96
+ const portMappings = this.ports.map(p => `-p ${p}:${p}`).join(' \\\n ')
97
+ return `
98
+ [Unit]
99
+ Description=Container service: ${this.name} (${this.protocol.toUpperCase()} ${this.ports.join(', ')})
100
+ After=network.target docker.service
101
+ Requires=docker.service
102
+
103
+ [Service]
104
+ EnvironmentFile=/etc/environment
105
+ ExecStartPre=-/usr/bin/docker rm -f ${this.name}
106
+ ExecStartPre=/usr/bin/docker pull ${this.image}
107
+ ExecStart=/usr/bin/docker run --name ${this.name} \\
108
+ --network ossy-network \\
109
+ ${portMappings} \\
110
+ --env-file /etc/environment \\
111
+ ${this.image}
112
+ ExecStop=/usr/bin/docker stop ${this.name}
113
+ Restart=on-failure
114
+ RestartSec=5
115
+
116
+ [Install]
117
+ WantedBy=multi-user.target
118
+ `
86
119
  }
87
120
 
88
- /**
89
- * Build ContainerService instances from the `services` array in platforms.json.
90
- * @param {ContainerServiceConfig[]} services
91
- * @returns {ContainerService[]}
92
- */
93
- static fromConfig(services = []) {
94
- return services.map((s, index) => new ContainerService(s, index))
121
+ /** TCP services bypass Caddy — no Caddy block. */
122
+ get caddyBlock() { return null }
123
+
124
+ /** Each port needs an explicit security group ingress rule. */
125
+ get securityGroupPorts() {
126
+ return this.ports.flatMap(port => {
127
+ if (this.protocol === 'both') return [{ port, protocol: 'tcp' }, { port, protocol: 'udp' }]
128
+ return [{ port, protocol: this.protocol }]
129
+ })
95
130
  }
96
131
 
132
+ get unitName() { return `${this.name}.service` }
133
+ install() { return [`sudo tee /etc/systemd/system/${this.unitName} > /dev/null << 'EOF'\n${this.systemdUnitFile}\nEOF`] }
134
+ enable() { return [`sudo systemctl enable ${this.name}`] }
135
+ start() { return [`sudo systemctl start ${this.name}`] }
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Factory
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /**
143
+ * Build service instances from the `services` array in platforms.json.
144
+ * HTTP services auto-assign host ports starting at 3002 (counted separately
145
+ * from TCP services so reordering TCP entries doesn't shift HTTP ports).
146
+ *
147
+ * @param {ContainerServiceConfig[]} services
148
+ * @returns {(HttpContainerService|TcpContainerService)[]}
149
+ */
150
+ const fromConfig = (services = []) => {
151
+ let httpIndex = 0
152
+ return services.map(s => {
153
+ if (s.type === 'tcp') return new TcpContainerService(s)
154
+ return new HttpContainerService(s, httpIndex++)
155
+ })
97
156
  }
98
157
 
99
158
  module.exports = {
100
- ContainerService
159
+ HttpContainerService,
160
+ TcpContainerService,
161
+ fromConfig
101
162
  }
@@ -10,7 +10,7 @@ ExecStartPre=-/usr/bin/docker rm -f ossy-api
10
10
  ExecStartPre=/usr/bin/docker pull ghcr.io/ossy-se/api:latest
11
11
  ExecStart=/usr/bin/docker run --name ossy-api \\
12
12
  --network ossy-network \\
13
- -p 3001:3001 \\
13
+ -p 3001:3000 \\
14
14
  -e DB_URL=\${DB_URL} \\
15
15
  -e DB_NAME=\${DB_NAME} \\
16
16
  -e TOKEN_SECRET=\${TOKEN_SECRET} \\