1{ config, lib, options, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.coder;
7 name = "coder";
8in {
9 options = {
10 services.coder = {
11 enable = mkEnableOption (lib.mdDoc "Coder service");
12
13 user = mkOption {
14 type = types.str;
15 default = "coder";
16 description = lib.mdDoc ''
17 User under which the coder service runs.
18
19 ::: {.note}
20 If left as the default value this user will automatically be created
21 on system activation, otherwise it needs to be configured manually.
22 :::
23 '';
24 };
25
26 group = mkOption {
27 type = types.str;
28 default = "coder";
29 description = lib.mdDoc ''
30 Group under which the coder service runs.
31
32 ::: {.note}
33 If left as the default value this group will automatically be created
34 on system activation, otherwise it needs to be configured manually.
35 :::
36 '';
37 };
38
39 package = mkOption {
40 type = types.package;
41 default = pkgs.coder;
42 description = lib.mdDoc ''
43 Package to use for the service.
44 '';
45 defaultText = literalExpression "pkgs.coder";
46 };
47
48 homeDir = mkOption {
49 type = types.str;
50 description = lib.mdDoc ''
51 Home directory for coder user.
52 '';
53 default = "/var/lib/coder";
54 };
55
56 listenAddress = mkOption {
57 type = types.str;
58 description = lib.mdDoc ''
59 Listen address.
60 '';
61 default = "127.0.0.1:3000";
62 };
63
64 accessUrl = mkOption {
65 type = types.nullOr types.str;
66 description = lib.mdDoc ''
67 Access URL should be a external IP address or domain with DNS records pointing to Coder.
68 '';
69 default = null;
70 example = "https://coder.example.com";
71 };
72
73 wildcardAccessUrl = mkOption {
74 type = types.nullOr types.str;
75 description = lib.mdDoc ''
76 If you are providing TLS certificates directly to the Coder server, you must use a single certificate for the root and wildcard domains.
77 '';
78 default = null;
79 example = "*.coder.example.com";
80 };
81
82 database = {
83 createLocally = mkOption {
84 type = types.bool;
85 default = true;
86 description = lib.mdDoc ''
87 Create the database and database user locally.
88 '';
89 };
90
91 host = mkOption {
92 type = types.str;
93 default = "/run/postgresql";
94 description = lib.mdDoc ''
95 Hostname hosting the database.
96 '';
97 };
98
99 database = mkOption {
100 type = types.str;
101 default = "coder";
102 description = lib.mdDoc ''
103 Name of database.
104 '';
105 };
106
107 username = mkOption {
108 type = types.str;
109 default = "coder";
110 description = lib.mdDoc ''
111 Username for accessing the database.
112 '';
113 };
114
115 password = mkOption {
116 type = types.nullOr types.str;
117 default = null;
118 description = lib.mdDoc ''
119 Password for accessing the database.
120 '';
121 };
122
123 sslmode = mkOption {
124 type = types.nullOr types.str;
125 default = "disable";
126 description = lib.mdDoc ''
127 Password for accessing the database.
128 '';
129 };
130 };
131
132 tlsCert = mkOption {
133 type = types.nullOr types.path;
134 description = lib.mdDoc ''
135 The path to the TLS certificate.
136 '';
137 default = null;
138 };
139
140 tlsKey = mkOption {
141 type = types.nullOr types.path;
142 description = lib.mdDoc ''
143 The path to the TLS key.
144 '';
145 default = null;
146 };
147 };
148 };
149
150 config = mkIf cfg.enable {
151 assertions = [
152 { assertion = cfg.database.createLocally -> cfg.database.username == name;
153 message = "services.coder.database.username must be set to ${user} if services.coder.database.createLocally is set true";
154 }
155 ];
156
157 systemd.services.coder = {
158 description = "Coder - Self-hosted developer workspaces on your infra";
159 after = [ "network.target" ];
160 wantedBy = [ "multi-user.target" ];
161
162 environment = {
163 CODER_ACCESS_URL = cfg.accessUrl;
164 CODER_WILDCARD_ACCESS_URL = cfg.wildcardAccessUrl;
165 CODER_PG_CONNECTION_URL = "user=${cfg.database.username} ${optionalString (cfg.database.password != null) "password=${cfg.database.password}"} database=${cfg.database.database} host=${cfg.database.host} ${optionalString (cfg.database.sslmode != null) "sslmode=${cfg.database.sslmode}"}";
166 CODER_ADDRESS = cfg.listenAddress;
167 CODER_TLS_ENABLE = optionalString (cfg.tlsCert != null) "1";
168 CODER_TLS_CERT_FILE = cfg.tlsCert;
169 CODER_TLS_KEY_FILE = cfg.tlsKey;
170 };
171
172 serviceConfig = {
173 ProtectSystem = "full";
174 PrivateTmp = "yes";
175 PrivateDevices = "yes";
176 SecureBits = "keep-caps";
177 AmbientCapabilities = "CAP_IPC_LOCK CAP_NET_BIND_SERVICE";
178 CacheDirectory = "coder";
179 CapabilityBoundingSet = "CAP_SYSLOG CAP_IPC_LOCK CAP_NET_BIND_SERVICE";
180 KillSignal = "SIGINT";
181 KillMode = "mixed";
182 NoNewPrivileges = "yes";
183 Restart = "on-failure";
184 ExecStart = "${cfg.package}/bin/coder server";
185 User = cfg.user;
186 Group = cfg.group;
187 };
188 };
189
190 services.postgresql = lib.mkIf cfg.database.createLocally {
191 enable = true;
192 ensureDatabases = [
193 cfg.database.database
194 ];
195 ensureUsers = [{
196 name = cfg.database.username;
197 ensurePermissions = {
198 "DATABASE \"${cfg.database.database}\"" = "ALL PRIVILEGES";
199 };
200 }
201 ];
202 };
203
204 users.groups = optionalAttrs (cfg.group == name) {
205 "${cfg.group}" = {};
206 };
207 users.users = optionalAttrs (cfg.user == name) {
208 ${name} = {
209 description = "Coder service user";
210 group = cfg.group;
211 home = cfg.homeDir;
212 createHome = true;
213 isSystemUser = true;
214 };
215 };
216 };
217}