diff --git a/src/nix/modules/composition/images.nix b/src/nix/modules/composition/images.nix
index c3c09a3..278716d 100644
--- a/src/nix/modules/composition/images.nix
+++ b/src/nix/modules/composition/images.nix
@@ -14,22 +14,11 @@ let
   addDetails = serviceName: service:
     builtins.addErrorContext "while evaluating the image for service ${serviceName}"
     (let
-      inherit (service) build;
+      imageAttrName = "image${lib.optionalString (service.image.tarball.isExe or false) "Exe"}";
     in {
-      imageName = build.imageName or service.image.name;
-      imageTag =
-                 if build.image.imageTag != ""
-                 then build.image.imageTag
-                 else lib.head (lib.strings.splitString "-" (baseNameOf build.image.outPath));
-    } // (if build.image.isExe or false
-        then {
-          imageExe = build.image.outPath;
-        }
-        else {
-          image = build.image.outPath;
-        }
-      )
-    );
+      inherit (service.image.tarball) imageName imageTag;
+      ${imageAttrName} = service.image.tarball.outPath;
+    });
 in
 {
   options = {
@@ -40,6 +29,21 @@ in
     };
   };
   config = {
+    assertions = let
+      assertionsForRepoTagComponent = component: attrName:
+        lib.mapAttrsToList (name: value: {
+          assertion = value.${attrName} != null;
+          message = lib.replaceStrings ["\n" ] [" "] ''
+            Unable to infer the ${component} of the image associated with
+            config.services.${name}.  Please set
+            config.services.${name}.image.${attrName} to a non-empty string.
+          '';
+        }) serviceImages;
+
+      nameAssertions = assertionsForRepoTagComponent "name" "imageName";
+      tagAssertions = assertionsForRepoTagComponent "tag" "imageTag";
+    in nameAssertions ++ tagAssertions;
+
     build.imagesToLoad = lib.attrValues serviceImages;
     docker-compose.extended.images = config.build.imagesToLoad;
   };
diff --git a/src/nix/modules/service/image.nix b/src/nix/modules/service/image.nix
index cc29578..6a9622d 100644
--- a/src/nix/modules/service/image.nix
+++ b/src/nix/modules/service/image.nix
@@ -35,13 +35,36 @@ let
     (pkgs.writeText "dummy-config.json" (builtins.toJSON config.image.rawConfig))
   ];
 
+  # Neither image names nor tags can can be empty strings; setting either to an
+  # empty string will cause `docker load` to croak with the error message
+  # "invalid reference format".
+  fallbackImageRepoTagComponent = component: fallback:
+    if (component != null && component != "")
+    then component
+    else fallback;
+  fallbackImageName = fallbackImageRepoTagComponent config.image.name;
+  fallbackImageTag = fallbackImageRepoTagComponent config.image.tag;
+
   # Shim for `services.<name>.image.tarball` definitions that refer to
   # arbitrary paths and not `dockerTools`-produced derivations.
-  dummyImagePackage = outPath: {
+  dummyImagePackage = outPath: let
+    baseNameNoExtension = lib.strings.removeSuffix ".tar.gz" (baseNameOf outPath);
+    baseNameComponents = lib.strings.splitString "-" baseNameNoExtension;
+    fallbacks =
+      if lib.isStorePath outPath
+      then {
+        imageName = lib.concatStringsSep "-" (lib.tail baseNameComponents);
+        imageTag = lib.head baseNameComponents;
+      }
+      else {
+        imageName = null;
+        imageTag = null;
+      };
+  in {
     inherit outPath;
     type = "derivation";
-    imageName = config.image.name;
-    imageTag = if config.image.tag == null then "" else config.image.tag;
+    imageName = fallbackImageName fallbacks.imageName;
+    imageTag = fallbackImageTag fallbacks.imageTag;
   };
 
   # Type matching the essential attributes of derivations produced by
@@ -99,11 +122,11 @@ let
 
   builtImage = buildOrStreamLayeredImage {
     inherit (config.image)
-      name
       tag
       contents
       includeStorePaths
       ;
+    name = fallbackImageName ("localhost/" + config.service.name);
     config = config.image.rawConfig;
     maxLayers = 100;
 
@@ -129,33 +152,6 @@ let
 in
 {
   options = {
-    build.image = mkOption {
-      type = nullOr package;
-      description = ''
-        Docker image derivation to be `docker load`-ed.
-
-        By default, when `services.<name>.image.nixBuild` is enabled, this is
-        the image produced using `services.<name>.image.command`,
-        `services.<name>.image.contents`, and
-        `services.<name>.image.rawConfig`.
-      '';
-      defaultText = lib.literalExample ''
-        pkgs.dockerTools.buildLayeredImage {
-          # ...
-        };
-      '';
-      internal = true;
-    };
-    build.imageName = mkOption {
-      type = str;
-      description = "Derived from `build.image`";
-      internal = true;
-    };
-    build.imageTag = mkOption {
-      type = str;
-      description = "Derived from `build.image`";
-      internal = true;
-    };
     image.nixBuild = mkOption {
       type = bool;
       description = ''
@@ -169,9 +165,8 @@ in
       '';
     };
     image.name = mkOption {
-      type = str;
-      default = "localhost/" + config.service.name;
-      defaultText = lib.literalExpression or lib.literalExample ''"localhost/" + config.service.name'';
+      type = nullOr str;
+      default = null;
       description = ''
         A human readable name for the Docker image.
 
@@ -258,6 +253,11 @@ in
         builder functions, or a Docker image tarball at some arbitrary
         location.
 
+        By default, when `services.<name>.image.nixBuild` is enabled, this is
+        the image produced using `services.<name>.image.command`,
+        `services.<name>.image.contents`, and
+        `services.<name>.image.rawConfig`.
+
         **Note** that using this option causes Arion to ignore most other
         options in the `services.<name>.image` namespace. The exceptions are
         `services.<name>.image.name` and `services.<name>.image.tag`, which are
@@ -266,34 +266,19 @@ in
       '';
       default = builtImage;
       example = lib.literalExample or lib.literalExpression ''
-        let
-          myimage = pkgs.dockerTools.buildImage {
-            name = "my-image";
-            contents = [ pkgs.coreutils ];
-          };
-        in
-        config.services = {
-          myservice = {
-            image.tarball = myimage;
-            # ...
-          };
-        }
+        pkgs.dockerTools.buildLayeredImage {
+          # ...
+        };
       '';
     };
   };
   config = lib.mkMerge [{
-      build.image = config.image.tarball;
-      build.imageName = config.build.image.imageName;
-      build.imageTag =
-                   if config.build.image.imageTag != ""
-                   then config.build.image.imageTag
-                   else lib.head (lib.strings.splitString "-" (baseNameOf config.build.image.outPath));
       image.rawConfig.Cmd = config.image.command;
       image.nixBuild = lib.mkDefault (priorityIsDefault options.service.image);
     }
     ( lib.mkIf (config.service.build.context == null)
     {
-      service.image = lib.mkDefault "${config.build.imageName}:${config.build.imageTag}";
+      service.image = lib.mkDefault "${config.image.tarball.imageName}:${config.image.tarball.imageTag}";
     })
   ];
 }